/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { Validate } from '../helper/Validate'
import { StringUtil } from '../internal/StringUtil'
import { Character } from '../util/Character'

/**
 * A character queue with parsing helpers.
 */
export class TokenQueue {
  private _queue: string
  private _pos = 0
  // escape char for chomp balanced.
  private static readonly ESC = '\\'

  constructor(data: string) {
    Validate.notNull(data)
    this._queue = data
  }

  /**
   * Is the queue empty?
   * @return true if no data left in queue.
   */
  public isEmpty(): boolean {
    return this.remainingLength() === 0
  }

  private remainingLength(): number {
    return this._queue.length - this._pos
  }

  /**
   Add a string to the start of the queue.
   @param seq string to add.
   */
  public addFirst(seq: string) {
    // not very performant, but an edge case
    this._queue = seq + this._queue.substring(this._pos)
    this._pos = 0
  }

  /**
   * Tests if the next characters on the queue match the sequence. Case insensitive.
   * @param seq String to check queue for.
   * @return true if the next characters match.
   */
  public matches(seq: string): boolean {
    return StringUtil.regionMatches(true, this._queue, this._pos, seq, 0, seq.length)
  }

  /**
   Tests if the next characters match any of the sequences. Case insensitive.
   @param seq list of strings to case insensitively check for
   @return true of any matched, false if none did
   */
  public matchesAny(...seq: string[]) {
    for (let s of seq) {
      if (this.matches(s)) {
        return true
      }
    }
    return false
  }

  /**
   * Tests if the queue matches the sequence (as with match), and if they do, removes the matched string from the
   * queue.
   * @param seq String to search for, and if found, remove from queue.
   * @return true if found and removed, false if not found.
   */
  public matchChomp(seq: string): boolean {
    if (this.matches(seq)) {
      this._pos += seq.length
      return true
    } else {
      return false
    }
  }

  /**
   Tests if queue starts with a whitespace character.
   @return if starts with whitespace
   */
  public matchesWhitespace(): boolean {
    return!this.isEmpty() && StringUtil.isWhitespace(this._queue.charCodeAt(this._pos))
  }

  /**
   Test if the queue matches a word character (letter or digit).
   @return if matches a word character
   */
  public matchesWord(): boolean {
    return!this.isEmpty() && Character.isLetterOrDigit(this._queue.charAt(this._pos))
  }

  /**
   * Drops the next character off the queue.
   */
  public advance() {
    if (!this.isEmpty()) {
      this._pos++
    }
  }

  /**
   * Consume one character off queue.
   * @return first character on queue.
   */
  public getConsume(): string {
    return this._queue.charAt(this._pos++)
  }

  /**
   * Consumes the supplied sequence of the queue. If the queue does not start with the supplied sequence, will
   * throw an illegal state exception -- but you should be running match() against that condition.
   <p>
   Case insensitive.
   * @param seq sequence to remove from head of queue.
   */
  public setConsume(seq: string) {
    if (!this.matches(seq)) {
      throw new Error("Queue did not match expected sequence")
    }
    let len = seq.length
    if (len > this.remainingLength()) {
      throw new Error("Queue not long enough to consume sequence")
    }
    this._pos += len
  }

  /**
   * Pulls a string off the queue, up to but exclusive of the match sequence, or to the queue running out.
   * @param seq String to end on (and not include in return, but leave on queue). <b>Case sensitive.</b>
   * @return The matched data consumed from queue.
   */
  public consumeTo(seq: string): string {
    let offset = this._queue.indexOf(seq, this._pos)
    if (offset !== -1) {
      let consumed = this._queue.substring(this._pos, offset)
      this._pos += consumed.length
      return consumed
    } else {
      return this.remainder()
    }
  }

  public consumeToIgnoreCase(seq: string): string {
    let start = this._pos
    let first = seq.substring(0, 1)
    // if first is not cased, use index of
    let canScan = first.toLowerCase() === first.toUpperCase()
    while (!this.isEmpty()) {
      if (this.matches(seq)) {
        break
      }
      if (canScan) {
        let skip = this._queue.indexOf(first, this._pos) - this._pos
        if (skip === 0) {
          // this char is the skip char, but not match, so force advance of pos
          this._pos++
        } else if (skip < 0) {
          // no chance of finding, grab to end
          this._pos = this._queue.length
        } else {
          this._pos += skip
        }
      } else {
        this._pos++
      }
    }
    return this._queue.substring(start, this._pos)
  }

  /**
   Consumes to the first sequence provided, or to the end of the queue. Leaves the terminator on the queue.
   @param seq any number of terminators to consume to. <b>Case insensitive.</b>
   @return consumed string
   */
  // todo: method name. not good that consumeTo cares for case, and consume to any doesn't. And the only use for this
  // is a case sensitive time...
  public consumeToAny(...seq: string[]): string {
    let start = this._pos
    while (!this.isEmpty() && !this.matchesAny(...seq)) {
      this._pos++
    }
    return this._queue.substring(start, this._pos)
  }

  /**
   * Pulls a string off the queue (like consumeTo), and then pulls off the matched string (but does not return it).
   * <p>
   * If the queue runs out of characters before finding the seq, will return as much as it can (and queue will go
   * isEmpty() == true).
   * @param seq String to match up to, and not include in return, and to pull off queue. <b>Case sensitive.</b>
   * @return Data matched from queue.
   */
  public chompTo(seq: string): string {
    let data = this.consumeTo(seq)
    this.matchChomp(seq)
    return data
  }

  public chompToIgnoreCase(seq: string): string {
    let data = this.consumeToIgnoreCase(seq)
    this.matchChomp(seq)
    return data
  }

  /**
   * Pulls a balanced string off the queue. E.g. if queue is "(one (two) three) four", (,) will return "one (two) three",
   * and leave " four" on the queue. Unbalanced openers and closers can be quoted (with ' or ") or escaped (with \). Those escapes will be left
   * in the returned string, which is suitable for regexes (where we need to preserve the escape), but unsuitable for
   * contains text strings; use unescape for that.
   * @param open opener
   * @param close closer
   * @return data matched from the queue
   */
  public chompBalanced(open: string, close: string): string {
    let start = -1
    let end = -1
    let depth = 0
    let last = 0
    let inSingleQuote = false
    let inDoubleQuote = false
    let inRegexQE = false
    do {
      if (this.isEmpty()) {
        break
      }
      let c = this.getConsume()
      if (last !== TokenQueue.ESC.charCodeAt(0)) {
        if (c === '\'' && c !== open && !inDoubleQuote) {
          inSingleQuote = !inSingleQuote
        } else if (c === '"' && c !== open && !inSingleQuote) {
          inDoubleQuote = !inDoubleQuote
        }
        if (inSingleQuote || inDoubleQuote || inRegexQE) {
          last = c.charCodeAt(0)
          continue
        }
        if (c === open) {
          depth++
          if (start === -1) {
            start = this._pos
          }
        } else if (c === close) {
          depth--
        }
      } else if (c === 'Q') {
        inRegexQE = true
      } else if (c === 'E') {
        inRegexQE = false
      }
      if (depth > 0 && last !== 0) {
        // don't include the outer match pair in the return
        end = this._pos
      }
      last = c.charCodeAt(0)
    } while (depth > 0)
    let out = (end >= 0) ? this._queue.substring(start, end) : ""
    if (depth > 0) {
      // ran out of queue before seeing enough )
      Validate.fail(`Did not find balanced marker at "${out}"`)
    }
    return out
  }

  /**
   * Unescape a \ escaped string.
   * @param in backslash escaped string
   * @return unescaped string
   */
  public static unescape(inStr: string): string {
    let out = StringUtil.borrowBuilder()
    let last = 0
    for (let c of inStr) {
      if (c === TokenQueue.ESC) {
        if (last === TokenQueue.ESC.charCodeAt(0)) {
          out.push(c)
          c = String.fromCharCode(0)
        }
      } else {
        out.push(c)
      }
      last = c.charCodeAt(0)
    }
    return StringUtil.releaseBuilder(out)
  }

  /*
    Given a CSS identifier (such as a tag, ID, or class), escape any CSS special characters that would otherwise not be
    valid in a selector.
     */
  public static escapeCssIdentifier(inStr: string): string {
    let out = StringUtil.borrowBuilder()
    let q = new TokenQueue(inStr)
    while (!q.isEmpty()) {
      if (q.matchesCssIdentifier(...TokenQueue._ElementSelectorChars)) {
        out.push(q.getConsume())
      } else {
        out.push(TokenQueue.ESC, q.getConsume())
      }
    }
    return StringUtil.releaseBuilder(out)
  }

  /**
   * Pulls the next run of whitespace characters of the queue.
   * @return Whether consuming whitespace or not
   */
  public consumeWhitespace():boolean {
    let seen = false
    while (this.matchesWhitespace()) {
      this._pos++
      seen = true
    }
    return seen
  }

  /**
   * Retrieves the next run of word type (letter or digit) off the queue.
   * @return String of word characters from queue, or empty string if none.
   */
  public consumeWord():string {
    let start = this._pos
    while (this.matchesWord()) {
      this._pos++
    }
    return this._queue.substring(start, this._pos)
  }

  /**
   * Consume a CSS element selector (tag name, but | instead of : for namespaces (or *| for wildcard namespace), to not conflict with :pseudo selects).
   *
   * @return tag name
   */
  public consumeElementSelector():string {
    return this.consumeEscapedCssIdentifier(...TokenQueue._ElementSelectorChars)
  }

  private static readonly _ElementSelectorChars = ["*|", "|", "_", "-"]

  /**
   Consume a CSS identifier (ID or class) off the queue (letter, digit, -, _)
   http://www.w3.org/TR/CSS2/syndata.html#value-def-identifier
   @return identifier
   */
  public consumeCssIdentifier():string {
    return this.consumeEscapedCssIdentifier(...TokenQueue._CssIdentifierChars)
  }

  private static readonly _CssIdentifierChars = ["-", "_"]

  private consumeEscapedCssIdentifier(...matches:string[]):string {
    let start = this._pos
    let escaped = false
    while (!this.isEmpty()) {
      if (this._queue.charAt(this._pos) === TokenQueue.ESC && this.remainingLength() > 1) {
        escaped = true
        this._pos +=2
      } else if (this.matchesCssIdentifier(...matches)) {
        this._pos++
      } else {
        break
      }
    }
    let consumed = this._queue.substring(start, this._pos)
    return escaped ? TokenQueue.unescape(consumed):consumed
  }

  private matchesCssIdentifier(...matches:string[]) {
    return this.matchesWord() || this.matchesAny(...matches)
  }

  /**
   Consume and return whatever is left on the queue.
   @return remained of queue.
   */
  public remainder():string {
    let remainder = this._queue.substring(this._pos)
    this._pos = this._queue.length
    return remainder
  }

  public toString():string {
    return this._queue.substring(this._pos)
  }

  public getQueue():string {
    return this._queue
  }

}

